/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.olingo.commons.core.serialization;

import com.fasterxml.jackson.databind.JsonNode;
import org.apache.olingo.commons.api.Constants;
import org.apache.olingo.commons.api.data.GeoUtils;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.constants.ODataServiceVersion;
import org.apache.olingo.commons.api.edm.geo.Geospatial;
import org.apache.olingo.commons.api.edm.geo.GeospatialCollection;
import org.apache.olingo.commons.api.edm.geo.LineString;
import org.apache.olingo.commons.api.edm.geo.MultiLineString;
import org.apache.olingo.commons.api.edm.geo.MultiPoint;
import org.apache.olingo.commons.api.edm.geo.MultiPolygon;
import org.apache.olingo.commons.api.edm.geo.Point;
import org.apache.olingo.commons.api.edm.geo.Polygon;
import org.apache.olingo.commons.api.edm.geo.SRID;
import org.apache.olingo.commons.core.edm.EdmTypeInfo;
import org.apache.olingo.commons.core.edm.primitivetype.EdmDouble;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

class JsonGeoValueDeserializer {

  private final ODataServiceVersion version;

  public JsonGeoValueDeserializer(final ODataServiceVersion version) {
    this.version = version;
  }

  private Point point(final Iterator<JsonNode> itor, final EdmPrimitiveTypeKind type, final SRID srid) {
    Point point = null;

    if (itor.hasNext()) {
      point = new Point(GeoUtils.getDimension(type), srid);
      try {
        point.setX(EdmDouble.getInstance().valueOfString(itor.next().asText(), null, null,
            Constants.DEFAULT_PRECISION, Constants.DEFAULT_SCALE, null, Double.class));
        point.setY(EdmDouble.getInstance().valueOfString(itor.next().asText(), null, null,
            Constants.DEFAULT_PRECISION, Constants.DEFAULT_SCALE, null, Double.class));
      } catch (EdmPrimitiveTypeException e) {
        throw new IllegalArgumentException("While deserializing point coordinates as double", e);
      }
    }

    return point;
  }

  private MultiPoint multipoint(final Iterator<JsonNode> itor, final EdmPrimitiveTypeKind type, final SRID srid) {
    final MultiPoint multiPoint;

    if (itor.hasNext()) {
      final List<Point> points = new ArrayList<Point>();
      while (itor.hasNext()) {
        final Iterator<JsonNode> mpItor = itor.next().elements();
        points.add(point(mpItor, type, srid));
      }
      multiPoint = new MultiPoint(GeoUtils.getDimension(type), srid, points);
    } else {
      multiPoint = new MultiPoint(GeoUtils.getDimension(type), srid, Collections.<Point> emptyList());
    }

    return multiPoint;
  }

  private LineString lineString(final Iterator<JsonNode> itor, final EdmPrimitiveTypeKind type, final SRID srid) {
    final LineString lineString;

    if (itor.hasNext()) {
      final List<Point> points = new ArrayList<Point>();
      while (itor.hasNext()) {
        final Iterator<JsonNode> mpItor = itor.next().elements();
        points.add(point(mpItor, type, srid));
      }
      lineString = new LineString(GeoUtils.getDimension(type), srid, points);
    } else {
      lineString = new LineString(GeoUtils.getDimension(type), srid, Collections.<Point> emptyList());
    }

    return lineString;
  }

  private MultiLineString multiLineString(final Iterator<JsonNode> itor, final EdmPrimitiveTypeKind type,
      final SRID srid) {

    final MultiLineString multiLineString;

    if (itor.hasNext()) {
      final List<LineString> lineStrings = new ArrayList<LineString>();
      while (itor.hasNext()) {
        final Iterator<JsonNode> mlsItor = itor.next().elements();
        lineStrings.add(lineString(mlsItor, type, srid));
      }
      multiLineString = new MultiLineString(GeoUtils.getDimension(type), srid, lineStrings);
    } else {
      multiLineString = new MultiLineString(GeoUtils.getDimension(type), srid, Collections.<LineString> emptyList());
    }

    return multiLineString;
  }

  private Polygon polygon(final Iterator<JsonNode> itor, final EdmPrimitiveTypeKind type, final SRID srid) {
    List<Point> extPoints = null;
    if (itor.hasNext()) {
      final Iterator<JsonNode> extItor = itor.next().elements();
      if (extItor.hasNext()) {
        extPoints = new ArrayList<Point>();
        while (extItor.hasNext()) {
          final Iterator<JsonNode> mpItor = extItor.next().elements();
          extPoints.add(point(mpItor, type, srid));
        }
      }
    }

    List<Point> intPoints = null;
    if (itor.hasNext()) {
      final Iterator<JsonNode> intItor = itor.next().elements();
      if (intItor.hasNext()) {
        intPoints = new ArrayList<Point>();
        while (intItor.hasNext()) {
          final Iterator<JsonNode> mpItor = intItor.next().elements();
          intPoints.add(point(mpItor, type, srid));
        }
      }
    }

    return new Polygon(GeoUtils.getDimension(type), srid, intPoints, extPoints);
  }

  private MultiPolygon multiPolygon(final Iterator<JsonNode> itor, final EdmPrimitiveTypeKind type, final SRID srid) {
    final MultiPolygon multiPolygon;

    if (itor.hasNext()) {
      final List<Polygon> polygons = new ArrayList<Polygon>();
      while (itor.hasNext()) {
        final Iterator<JsonNode> mpItor = itor.next().elements();
        polygons.add(polygon(mpItor, type, srid));
      }
      multiPolygon = new MultiPolygon(GeoUtils.getDimension(type), srid, polygons);
    } else {
      multiPolygon = new MultiPolygon(GeoUtils.getDimension(type), srid, Collections.<Polygon> emptyList());
    }

    return multiPolygon;
  }

  private GeospatialCollection collection(final Iterator<JsonNode> itor, final EdmPrimitiveTypeKind type,
      final SRID srid) {

    final GeospatialCollection collection;

    if (itor.hasNext()) {
      final List<Geospatial> geospatials = new ArrayList<Geospatial>();

      while (itor.hasNext()) {
        final JsonNode geo = itor.next();
        final String collItemType = geo.get(Constants.ATTR_TYPE).asText();
        final String callAsType;
        if (EdmPrimitiveTypeKind.GeographyCollection.name().equals(collItemType)
            || EdmPrimitiveTypeKind.GeometryCollection.name().equals(collItemType)) {

          callAsType = collItemType;
        } else {
          callAsType = (type == EdmPrimitiveTypeKind.GeographyCollection ? "Geography" : "Geometry")
              + collItemType;
        }

        geospatials.add(deserialize(geo, new EdmTypeInfo.Builder().setTypeExpression(callAsType).build()));
      }

      collection = new GeospatialCollection(GeoUtils.getDimension(type), srid, geospatials);
    } else {
      collection = new GeospatialCollection(GeoUtils.getDimension(type), srid, Collections.<Geospatial> emptyList());
    }

    return collection;
  }

  public Geospatial deserialize(final JsonNode node, final EdmTypeInfo typeInfo) {
    final EdmPrimitiveTypeKind actualType;
    if ((typeInfo.getPrimitiveTypeKind() == EdmPrimitiveTypeKind.Geography
        || typeInfo.getPrimitiveTypeKind() == EdmPrimitiveTypeKind.Geometry)
        && node.has(Constants.ATTR_TYPE)) {

      String nodeType = node.get(Constants.ATTR_TYPE).asText();
      if (nodeType.startsWith("Geo")) {
        final int yIdx = nodeType.indexOf('y');
        nodeType = nodeType.substring(yIdx + 1);
      }
      actualType = EdmPrimitiveTypeKind.valueOfFQN(version, typeInfo.getFullQualifiedName().toString() + nodeType);
    } else {
      actualType = typeInfo.getPrimitiveTypeKind();
    }

    final Iterator<JsonNode> cooItor = node.has(Constants.JSON_COORDINATES)
        ? node.get(Constants.JSON_COORDINATES).elements()
        : Collections.<JsonNode> emptyList().iterator();

    SRID srid = null;
    if (node.has(Constants.JSON_CRS)) {
      srid = SRID.valueOf(
          node.get(Constants.JSON_CRS).get(Constants.PROPERTIES).get(Constants.JSON_NAME).asText().split(":")[1]);
    }

    Geospatial value = null;
    switch (actualType) {
    case GeographyPoint:
    case GeometryPoint:
      value = point(cooItor, actualType, srid);
      break;

    case GeographyMultiPoint:
    case GeometryMultiPoint:
      value = multipoint(cooItor, actualType, srid);
      break;

    case GeographyLineString:
    case GeometryLineString:
      value = lineString(cooItor, actualType, srid);
      break;

    case GeographyMultiLineString:
    case GeometryMultiLineString:
      value = multiLineString(cooItor, actualType, srid);
      break;

    case GeographyPolygon:
    case GeometryPolygon:
      value = polygon(cooItor, actualType, srid);
      break;

    case GeographyMultiPolygon:
    case GeometryMultiPolygon:
      value = multiPolygon(cooItor, actualType, srid);
      break;

    case GeographyCollection:
    case GeometryCollection:
      value = collection(node.get(Constants.JSON_GEOMETRIES).elements(), actualType, srid);
      break;

    default:
    }

    return value;
  }
}
